Indice della Lezione
🎯 Introduzione
Le stringhe rappresentano uno degli elementi più utilizzati nella programmazione, essendo fondamentali per la gestione di testi, nomi, messaggi e qualsiasi tipo di informazione testuale. A differenza di molti linguaggi di programmazione moderni che forniscono un tipo di dato dedicato per le stringhe, il linguaggio C adotta un approccio più "primitivo" e a basso livello: le stringhe sono array di caratteri terminati da un carattere speciale chiamato carattere null ('\0').
Questa scelta progettuale, sebbene possa sembrare limitante rispetto ad approcci più moderni, riflette la filosofia del C di fornire accesso diretto e controllo completo sulla memoria. Comprendere come funzionano le stringhe in C è essenziale non solo per utilizzare questo linguaggio, ma anche per capire come molti altri linguaggi gestiscono internamente le stringhe e per sviluppare una comprensione profonda della gestione della memoria.
💡 Obiettivi della Lezione
In questa lezione approfondiremo i seguenti argomenti:
- Comprendere cosa sono le stringhe in C e come sono rappresentate in memoria
- Imparare a dichiarare, inizializzare e gestire stringhe in C
- Utilizzare le funzioni della libreria standard
string.h - Confrontare stringhe e manipolarle in modo sicuro
- Evitare errori comuni e vulnerabilità di sicurezza
- Gestire l'input/output di stringhe correttamente
- Allocare dinamicamente stringhe quando necessario
📚 Concetti Fondamentali
1.1 Cosa Sono le Stringhe in C
In C, una stringa è definita come un array di caratteri terminato dal carattere null ('\0'). Il carattere null, con valore ASCII 0, è fondamentale perché indica la fine della stringa. Senza questo terminatore, le funzioni C non saprebbero dove la stringa termina, portando a comportamenti indefiniti.
Rappresentazione in Memoria di una Stringa
Stringa: "CIAO"
Nota: La stringa "CIAO" occupa 5 byte in memoria: 4 caratteri più il terminatore null.
I numeri sotto ogni carattere rappresentano i valori ASCII corrispondenti.
Il carattere null '\0' non è lo stesso dello spazio ' ' o del carattere zero '0':
'\0'ha valore ASCII 0 (terminatore di stringa)' 'ha valore ASCII 32 (carattere spazio visibile)'0'ha valore ASCII 48 (carattere cifra zero)
1.2 Differenza tra Carattere e Stringa
È fondamentale comprendere la distinzione tra un singolo carattere e una stringa in C, poiché vengono gestiti in modi completamente diversi.
Carattere Singolo (char)
Un carattere singolo si definisce con apici singoli ' ' e occupa esattamente 1 byte in memoria. Rappresenta un singolo valore ASCII.
char lettera = 'A'; // Un byte
char cifra = '7'; // Un byte
char simbolo = '@'; // Un byte
Stringa (array di char)
Una stringa si definisce con apici doppi " " e occupa N+1 byte in memoria, dove N è il numero di caratteri visibili e +1 è per il terminatore null.
char parola[] = "A"; // 2 byte: 'A' + '\0'
char numero[] = "7"; // 2 byte: '7' + '\0'
char simboli[] = "@"; // 2 byte: '@' + '\0'
Non confondere la sintassi:
char x = 'A'; // CORRETTO: carattere singolo
char y = "A"; // ERRORE: stai assegnando un puntatore a char
char z[] = 'A'; // ERRORE: gli array si inizializzano con stringhe o liste
char w[] = "A"; // CORRETTO: stringa di un carattere + '\0'
📋 Dichiarazione e Inizializzazione
2.1 Metodi di Dichiarazione
Esistono diversi modi per dichiarare e inizializzare stringhe in C. Ogni metodo ha le sue caratteristiche e casi d'uso specifici. Vediamoli in dettaglio.
2.1.1 Inizializzazione con Stringa Letterale
Il metodo più comune e diretto è utilizzare una stringa letterale tra doppi apici. Il compilatore calcola automaticamente la dimensione necessaria e aggiunge il terminatore null.
// Dimensione automatica - il compilatore conta i caratteri e aggiunge '\0'
char nome[] = "Mario";
// Equivale a: char nome[6] = {'M', 'a', 'r', 'i', 'o', '\0'};
// Dimensione: 6 byte (5 caratteri + 1 terminatore)
char saluto[] = "Ciao mondo!";
// Dimensione: 12 byte (11 caratteri + 1 terminatore)
char vuota[] = "";
// Dimensione: 1 byte (solo il terminatore '\0')
Lascia che il compilatore calcoli la dimensione dell'array quando possibile. È più sicuro e meno soggetto a errori.
2.1.2 Dichiarazione con Dimensione Esplicita
Puoi specificare esplicitamente la dimensione dell'array. Questo è utile quando vuoi riservare spazio per stringhe che potrebbero crescere o per buffer di input.
// Dimensione esplicita maggiore del necessario
char cognome[50] = "Rossi";
// cognome[0]='R', cognome[1]='o', cognome[2]='s', cognome[3]='s',
// cognome[4]='i', cognome[5]='\0', cognome[6...49]=0
// Buffer per input utente
char input[256] = ""; // Inizializzato vuoto, pronto per ricevere input
// Array per costruire stringhe dinamicamente
char messaggio[100]; // Non inizializzato - PERICOLOSO!
char buffer[100] = {0}; // Inizializzato tutto a '\0' - SICURO!
Se la dimensione specificata è troppo piccola per contenere la stringa e il terminatore null, otterrai un errore di compilazione:
char errore[3] = "CIAO"; // ERRORE: servono 5 byte, ne hai 3!
2.1.3 Inizializzazione Carattere per Carattere
2.1.4 Puntatori a Stringhe Letterali
💡 Array vs Puntatore
Questa è una distinzione fondamentale:
char str1[] = "Hello"; // Array modificabile in memoria stack
char *str2 = "Hello"; // Puntatore a stringa in memoria read-only
str1[0] = 'h'; // OK: modifica la copia in memoria stack
str2[0] = 'h'; // ERRORE: tenta di modificare memoria read-only!
2.2 Stringhe e Array nella Lezione sugli Array
Per una comprensione completa degli array in C, inclusa la rappresentazione in memoria, l'aritmetica dei puntatori e gli array multidimensionali, consulta la lezione dedicata agli Array in C. Le stringhe sono casi speciali di array di caratteri, quindi tutti i concetti sugli array si applicano anche alle stringhe.
🛠️ Funzioni della Libreria string.h
La libreria standard C fornisce numerose funzioni per manipolare le stringhe, tutte dichiarate nell'header <string.h>. Queste funzioni sono ottimizzate e affidabili, e dovrebbero essere preferite a implementazioni personalizzate nella maggior parte dei casi.
3.1 Lunghezza di una Stringa - strlen()
La funzione strlen() restituisce il numero di caratteri nella stringa, escludendo il terminatore null. Questa funzione scorre la stringa finché non trova '\0'.
strlen() restituisce un valore di tipo size_t, un tipo unsigned definito in <stddef.h>. Per stamparlo correttamente, usa il formato %zu con printf().
3.2 Copiare Stringhe - strcpy() e strncpy()
Per copiare una stringa in un'altra, C fornisce strcpy(). È importante che la destinazione abbia spazio sufficiente per contenere l'intera stringa sorgente più il terminatore null.
3.2.1 strcpy() - Copia Standard
strcpy() non controlla la dimensione del buffer di destinazione! Se la stringa sorgente è più lunga dello spazio disponibile, si verifica un buffer overflow, causando comportamenti indefiniti:
char piccolo[5];
strcpy(piccolo, "Questa stringa è troppo lunga");
// DISASTRO: scrive oltre i limiti di piccolo!
// Possibili conseguenze: corruzione memoria, crash, vulnerabilità sicurezza
3.2.2 strncpy() - Copia Sicura con Limite
strncpy() ha un comportamento particolare:
- Se la sorgente è più corta di n caratteri, riempie il resto con
'\0' - Se la sorgente è più lunga di n caratteri, NON aggiunge automaticamente '\0' alla fine!
Devi sempre aggiungere manualmente il terminatore null per sicurezza.
3.3 Concatenare Stringhe - strcat() e strncat()
3.3.1 strcat() - Concatenazione Standard
3.3.2 strncat() - Concatenazione Sicura
A differenza di strncpy(), strncat() aggiunge sempre il terminatore null automaticamente!
3.4 Confrontare Stringhe - strcmp() e strncmp()
In C, non puoi confrontare stringhe con gli operatori ==, <, > ecc. Questi operatori confronterebbero gli indirizzi di memoria, non il contenuto. Devi usare strcmp().
3.4.1 strcmp() - Confronto Completo
strcmp() confronta due stringhe lessicograficamente (ordine alfabetico) e restituisce:
- 0 se le stringhe sono identiche
- Valore negativo se la prima stringa precede la seconda nell'ordine alfabetico
- Valore positivo se la prima stringa segue la seconda
strcmp() distingue tra maiuscole e minuscole:
strcmp("Hello", "hello") // Restituisce un valore ≠ 0
strcmp("HELLO", "hello") // Restituisce un valore negativo
3.5 Cercare in una Stringa - strchr(), strstr()
3.5.1 strchr() - Cerca un Carattere
3.5.2 strstr() - Cerca una Sottostringa
3.6 Tokenizzare una Stringa - strtok()
strtok() modifica la stringa originale sostituendo i delimitatori con '\0'. Se devi preservare la stringa originale, fai una copia prima.
3.7 Tabella Riepilogativa delle Funzioni
| Funzione | Descrizione | Utilizzo |
|---|---|---|
strlen(str) |
Restituisce la lunghezza (esclude '\0') | Calcolare dimensione stringa |
strcpy(dest, src) |
Copia src in dest | Duplicare stringhe (ATTENZIONE: buffer overflow) |
strncpy(dest, src, n) |
Copia max n caratteri | Copia sicura con limite |
strcat(dest, src) |
Concatena src a dest | Unire stringhe |
strncat(dest, src, n) |
Concatena max n caratteri | Concatenazione sicura |
strcmp(s1, s2) |
Confronta due stringhe | Verifica uguaglianza |
strchr(str, ch) |
Trova prima occorrenza di ch | Cercare un carattere |
strstr(str, substr) |
Trova prima occorrenza di substr | Cercare una sottostringa |
strtok(str, delim) |
Divide stringa in token | Parsing di testi |
⌨️ Input e Output di Stringhe
4.1 Output con printf()
#include <stdio.h>
int main() {
char nome[] = "Mario";
char cognome[] = "Rossi";
printf("Nome: %s\n", nome);
printf("%s %s\n", nome, cognome);
printf("Prime 3 lettere: %.3s\n", nome);
return 0;
}
4.2 Input Sicuro con fgets()
- Legge l'intera linea, inclusi gli spazi
- Specifica la dimensione massima del buffer
- Non causa buffer overflow
🔧 Allocazione Dinamica di Stringhe
⚠️ Errori Comuni e Come Evitarli
char buffer[10];
strcpy(buffer, "Stringa troppo lunga"); // DISASTRO!
Soluzione: Usa strncpy() o controlla le dimensioni.
char str[4] = {'C', 'I', 'A', 'O'}; // Manca '\0'!
printf("%s\n", str); // Comportamento indefinito!
char str1[] = "Hello";
char str2[] = "Hello";
if (str1 == str2) { // SBAGLIATO: confronta indirizzi!
// Usa strcmp(str1, str2) == 0
}
✅ Best Practices per Stringhe Sicure
- Inizializza sempre:
char buffer[100] = {0}; - Usa funzioni "n": Preferisci
strncpy(),strncat() - Controlla i limiti: Verifica sempre lo spazio disponibile
- Usa sizeof():
strncpy(dest, src, sizeof(dest) - 1); - Preferisci fgets() a scanf(): Per input utente
- Controlla malloc(): Verifica sempre
!= NULL - Libera la memoria: Ogni
malloc()ha il suofree()
💪 Esercizi Proposti
Esercizio 1: Inversione di Stringa
Scrivi una funzione che inverte una stringa sul posto.
void inverti_stringa(char *str);
// Input: "Ciao"
// Output: "oaiC"
Esercizio 2: Conta Vocali
Conta il numero di vocali e consonanti in una stringa.
Esercizio 3: Palindromo
Verifica se una stringa è un palindromo.
int e_palindromo(const char *str);
// "anna" → 1
// "radar" → 1
// "ciao" → 0
🎯 Quiz di Verifica
Domanda 1
Quale carattere termina una stringa in C?
Domanda 2
Quale funzione è più sicura per copiare stringhe?
Domanda 3
Come si confrontano correttamente due stringhe?
🎯 Conclusioni
Le stringhe in C, sebbene implementate in modo più "primitivo" rispetto ad altri linguaggi, offrono un controllo completo e prestazioni eccellenti. La chiave per lavorare efficacemente con le stringhe in C è comprendere:
- Come sono rappresentate in memoria (array + terminatore null)
- I pericoli del buffer overflow e come evitarli
- Le funzioni della libreria standard
- La gestione corretta della memoria
- Fondamenti: Le stringhe sono array di char terminati da '\0'
- Funzioni standard: strlen(), strcpy(), strcmp() e varianti
- Sicurezza: Evitare buffer overflow, usare funzioni "n"
- Input/Output: Preferire fgets() a scanf()
📚 Riferimenti e Approfondimenti
Per approfondire ulteriormente gli argomenti trattati, consultare:
- Brian W. Kernighan, Dennis M. Ritchie - Il Linguaggio C - Pearson
- Stephen Prata - C Primer Plus - Sesta edizione
- GNU C Library: https://www.gnu.org/software/libc/manual/
- C String Reference: https://en.cppreference.com/w/c/string